package cn.orangeframe.neon.application.tasks;

import cn.orangeframe.neon.application.run.ScriptEvaluator;
import cn.orangeframe.neon.application.run.WorkflowExecutor;
import cn.orangeframe.neon.application.util.ParametersUtils;
import cn.orangeframe.neon.application.util.TaskUtils;
import cn.orangeframe.neon.domain.entity.WorkflowTask;
import cn.orangeframe.neon.domain.enums.TaskStatus;
import cn.orangeframe.neon.domain.enums.TaskType;
import cn.orangeframe.neon.domain.model.TaskModel;
import cn.orangeframe.neon.domain.model.WorkflowModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.script.ScriptException;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Component
public class DoWhile extends WorkflowSystemTask {

    private final ParametersUtils parametersUtils;

    public DoWhile(ParametersUtils parametersUtils) {
        super(TaskType.TASK_TYPE_DO_WHILE);
        this.parametersUtils = parametersUtils;
    }

    @Override
    public void execute(WorkflowModel workflow, TaskModel doWhileTaskModel, WorkflowExecutor executor) {
        /*
         * Get the latest set of tasks (the ones that have the highest retry count). We don't want to evaluate any tasks
         * that have already failed if there is a more current one (a later retry count).
         * 获取最新的一组任务（重试次数最多的任务）。我们不想评估任何任务
         * 如果有更新的重试计数（稍后的重试计数），则已经失败。
         */
        Map<String, TaskModel> relevantTasks = new LinkedHashMap<>();
        TaskModel relevantTask;
        for (TaskModel t : workflow.getTasks()) {
            if (doWhileTaskModel
                    .getWorkflowTask()
                    .has(TaskUtils.removeIterationFromTaskRefName(t.getTaskReferenceName()))
                    && !doWhileTaskModel.getTaskReferenceName().equals(t.getTaskReferenceName())
                    && doWhileTaskModel.getIteration() == t.getIteration()) {
                relevantTask = relevantTasks.get(t.getTaskReferenceName());
                if (relevantTask == null || t.getRetryCount() > relevantTask.getRetryCount()) {
                    relevantTasks.put(t.getTaskReferenceName(), t);
                }
            }
        }
        Collection<TaskModel> loopOverTasks = relevantTasks.values();

        // if the loopOverTasks collection is empty, no tasks inside the loop have been scheduled.
        // so schedule it and exit the method.
        if (loopOverTasks.isEmpty()) {
            doWhileTaskModel.setIteration(1);
            doWhileTaskModel.addOutput("iteration", doWhileTaskModel.getIteration());
            scheduleNextIteration(doWhileTaskModel, workflow, executor);
            return;
        }

        if (!isIterationComplete(doWhileTaskModel, relevantTasks)) {
            // current iteration is not complete (all tasks inside the loop are not terminal)
            return;
        }

        boolean shouldContinue;
        try {
            shouldContinue = evaluateCondition(workflow, doWhileTaskModel);
            if (shouldContinue) {
                doWhileTaskModel.setIteration(doWhileTaskModel.getIteration() + 1);
                doWhileTaskModel.addOutput("iteration", doWhileTaskModel.getIteration());
                scheduleNextIteration(doWhileTaskModel, workflow, executor);
            } else {
                markTaskSuccess(doWhileTaskModel);
            }
        } catch (ScriptException e) {
            String message = String.format(
                        "Unable to evaluate condition %s, exception %s",
                        doWhileTaskModel.getWorkflowTask().getLoopCondition(), e.getMessage());
            log.error(message);
            markTaskFailure(doWhileTaskModel, message);
        }
    }

    private void scheduleNextIteration(
            TaskModel doWhileTaskModel, WorkflowModel workflow, WorkflowExecutor workflowExecutor) {
        log.debug(
                "Scheduling loop tasks for task {} as condition {} evaluated to true",
                doWhileTaskModel.getTaskId(),
                doWhileTaskModel.getWorkflowTask().getLoopCondition());
        workflowExecutor.scheduleNextIteration(doWhileTaskModel, workflow);
    }

    private void markTaskSuccess(TaskModel taskModel) {
        log.debug(
                "Task {} took {} iterations to complete",
                taskModel.getTaskId(),
                taskModel.getIteration() + 1);
        taskModel.setStatus(TaskStatus.COMPLETED);
    }

    private void markTaskFailure(TaskModel taskModel, String failureReason) {
        log.error("Marking task {} failed with error.", taskModel.getTaskId());
        taskModel.setReasonForInCompletion(failureReason);
        taskModel.setStatus(TaskStatus.FAILED_WITH_TERMINAL_ERROR);
    }

    boolean evaluateCondition(WorkflowModel workflow, TaskModel task) throws ScriptException {
        // Use paramUtils to compute the task input
        Map<String, Object> conditionInput =
                parametersUtils.getTaskInput(
                        task.getWorkflowTask().getInputParameters(),
                        workflow);
        conditionInput.put(task.getTaskReferenceName(), task.getOutputData());
        List<TaskModel> loopOver =
                workflow.getTasks().stream()
                        .filter(t -> (task.getWorkflowTask()
                                .has(TaskUtils.removeIterationFromTaskRefName(t.getTaskReferenceName()))
                                                && !task.getTaskReferenceName()
                                                .equals(t.getTaskReferenceName())))
                        .toList();

        for (TaskModel loopOverTask : loopOver) {
            conditionInput.put(
                    TaskUtils.removeIterationFromTaskRefName(loopOverTask.getTaskReferenceName()),
                    loopOverTask.getOutputData());
        }

        String condition = task.getWorkflowTask().getLoopCondition();
        boolean result = false;
        if (condition != null) {
            log.debug("Condition: {} is being evaluated", condition);
            // Evaluate the expression by using the Nashorn based script evaluator
            result = ScriptEvaluator.evalBool(condition, conditionInput);
        }
        return result;
    }

    private boolean isIterationComplete(
            TaskModel doWhileTaskModel, Map<String, TaskModel> referenceNameToModel) {
        List<WorkflowTask> workflowTasksInsideDoWhile =
                doWhileTaskModel.getWorkflowTask().getLoopOver();
        int iteration = doWhileTaskModel.getIteration();
        boolean allTasksTerminal = true;
        for (WorkflowTask workflowTaskInsideDoWhile : workflowTasksInsideDoWhile) {
            String taskReferenceName =
                    TaskUtils.appendIteration(
                            workflowTaskInsideDoWhile.getTaskReferenceName(), iteration);
            if (referenceNameToModel.containsKey(taskReferenceName)) {
                TaskModel taskModel = referenceNameToModel.get(taskReferenceName);
                if (!taskModel.getStatus().isTerminal()) {
                    allTasksTerminal = false;
                    break;
                }
            } else {
                allTasksTerminal = false;
                break;
            }
        }

        if (!allTasksTerminal) {
            // Cases where tasks directly inside loop over are not completed.
            // loopOver -> [task1 -> COMPLETED, task2 -> IN_PROGRESS]
            return false;
        }

        // Check all the tasks in referenceNameToModel are completed or not. These are set of tasks
        // which are not directly inside loopOver tasks, but they are under hierarchy
        // loopOver -> [decisionTask -> COMPLETED [ task1 -> COMPLETED, task2 -> IN_PROGRESS]]
        return referenceNameToModel.values().stream()
                .allMatch(taskModel -> taskModel.getStatus().isTerminal());
    }
}
